package org.tulkas.robot;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.Tag;
import org.htmlparser.filters.AndFilter;
import org.htmlparser.filters.OrFilter;
import org.htmlparser.filters.TagNameFilter;
import org.htmlparser.lexer.Lexer;
import org.htmlparser.nodes.TagNode;
import org.htmlparser.tags.FormTag;
import org.htmlparser.tags.InputTag;
import org.htmlparser.tags.MetaTag;
import org.htmlparser.tags.TitleTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tulkas.robot.http.BaseConn;
import org.tulkas.robot.http.ClientOptions;
import org.tulkas.robot.patch.CssSelectorNodeFilter;
import org.tulkas.robot.patch.HasAttributeICFilter;
import org.tulkas.util.HttpUtil;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class Page extends BaseConn {
	private static Logger logger = LoggerFactory.getLogger(Page.class);

	private static Pattern PTN_HEAD_END = Pattern.compile("</head>",
			Pattern.CASE_INSENSITIVE);

	/**
	 * HTML content of the page
	 */
	private String html;

	/**
	 * The charset of the page content
	 */
	private String charset;

	/**
	 * the title of the page
	 */
	private String title;

	private byte[] rawResponse;

	private List<FormHolder> forms = null;

	private final List<MetaHolder> metas = new ArrayList<MetaHolder>();

	public String getHtml() {
		return html;
	}

	public String getTitle() {
		return title;
	}

	public String getCharset() {
		return charset;
	}

	public void setCharset(String charset) throws UnsupportedEncodingException {
		if (this.charset != charset && this.html != null) {
			Charset.forName(charset);
			html = new String(rawResponse, charset);
		}
		this.charset = charset;
	}

	@Override
	public HttpRequestBase createHttpRequest(ClientOptions options) {
		HttpGet get = new HttpGet(this.getUrl());
		return get;
	}

	@Override
	public OutputStream createResponseStream() {
		return new ByteArrayOutputStream();
	}

	private String setupCharsetFromMeta(String head)
			throws UnsupportedEncodingException {
		Lexer lexer = new Lexer(new org.htmlparser.lexer.Page(head));
		Parser parser = new Parser(lexer, Parser.DEVNULL);

		TagNameFilter tnFilter = new TagNameFilter("meta");
		HasAttributeICFilter haFilter = new HasAttributeICFilter("http-equiv",
				"content-type");
		AndFilter filter = new AndFilter(tnFilter, haFilter);
		try {
			NodeList nl = parser.extractAllNodesThatMatch(filter);
			if (nl.size() > 0) {
				TagNode tag = (TagNode) nl.elementAt(0);
				String metaContentType = tag.getAttribute("content");
				if (metaContentType != null) {
					String metaCharset = HttpUtil
							.parseCharsetFromContentType(metaContentType);
					Charset ori = Charset.forName(charset);
					if (metaCharset != null
							&& ori.compareTo(Charset.forName(metaCharset)) != 0) {
						charset = metaCharset;
						html = new String(rawResponse, metaCharset);
						Matcher heMatcher = PTN_HEAD_END.matcher(html);
						if (heMatcher.find()) {
							int e = heMatcher.end();
							head = html.substring(0, e);
						}
					}
				}
			}
		} catch (ParserException e) {
			logger.debug(
					"Error parsing content-type <meta> from page <head>. ", e);
		}

		return head;
	}

	private void resolveMetaValues(String head) {
		Lexer lexer = new Lexer(new org.htmlparser.lexer.Page(head));
		Parser parser = new Parser(lexer, Parser.DEVNULL);

		TagNameFilter metaFilter = new TagNameFilter("meta");
		TagNameFilter titleFilter = new TagNameFilter("title");
		OrFilter filter = new OrFilter(metaFilter, titleFilter);

		try {
			NodeList nl = parser.extractAllNodesThatMatch(filter);
			for (int i = 0; i < nl.size(); i++) {
				Node node = nl.elementAt(i);
				if (node instanceof TitleTag) {
					title = ((TitleTag) node).getStringText();
				} else if (node instanceof MetaTag) {
					MetaTag meta = (MetaTag) node;
					MetaHolder mh = new MetaHolder();
					mh.name = meta.getMetaTagName();
					mh.httpEquiv = meta.getHttpEquiv();
					mh.content = meta.getMetaContent();
					metas.add(mh);
				}
			}
		} catch (ParserException e) {
			logger.debug(
					"Error parsing <meta>s and <title> from page <head>. ", e);
		}
	}

	@Override
	public void handleResponseContent(OutputStream outputStream,
			ClientOptions options) throws IOException {
		if (!(outputStream instanceof ByteArrayOutputStream))
			return;

		rawResponse = ((ByteArrayOutputStream) outputStream).toByteArray();
		outputStream.close();

		charset = options.getDefaultCharset();
		String contentType = this.getResponseHeaders().getContentType();
		boolean foundCharset = false;
		if (contentType != null) {
			String hdCharset = HttpUtil
					.parseCharsetFromContentType(contentType);
			if (hdCharset != null) {
				charset = hdCharset;
				foundCharset = true;
			}
		}

		if (rawResponse != null) {
			html = new String(rawResponse, charset);
			Matcher heMatcher = PTN_HEAD_END.matcher(html);
			String head = html;
			if (heMatcher.find()) {
				int e = heMatcher.end();
				head = html.substring(0, e);
			}

			if (!foundCharset) {
				head = setupCharsetFromMeta(head);
			}

			resolveMetaValues(head);
		}
	}

	private void extractHiddenInput(Form f, FormTag ft) {
		Tag t = ft.getEndTag();
		if (t != null) {
			int s = ft.getStartPosition();
			int e = t.getEndPosition();
			String formText = html.substring(s, e);
			Lexer lexer = new Lexer(new org.htmlparser.lexer.Page(formText));
			Parser parser = new Parser(lexer, Parser.DEVNULL);
			try {
				NodeList nl = parser
						.extractAllNodesThatMatch(new CssSelectorNodeFilter(
								"input[type='hidden']"));
				for (int i = 0; i < nl.size(); i++) {
					Node n = nl.elementAt(i);
					if (n instanceof InputTag) {
						InputTag input = (InputTag) n;
						String name = input.getAttribute("name");
						String value = input.getAttribute("value");
						f.addValue(name, value);
					}
				}
			} catch (ParserException e1) {
			}
		}
	}

	private FormHolder buildFormHolder(FormTag ft) {
		FormHolder fh = new FormHolder();
		fh.id = ft.getAttribute("id");
		fh.name = ft.getAttribute("name");
		Form f = new Form();
		fh.form = f;
		String v = ft.getAttribute("action");
		f.setAction(v);
		v = ft.getAttribute("method");
		if (v != null)
			f.setMethod(v);
		v = ft.getAttribute("enctype");
		if (v != null)
			f.setEnctype(v);
		v = ft.getAttribute("accept-charset");
		if (v != null && "unknown".equalsIgnoreCase(v))
			f.setCharset(v);
		else
			f.setCharset(charset);

		extractHiddenInput(f, ft);

		return fh;
	}

	private List<FormHolder> getFormList() {
		if (forms == null) {
			forms = new ArrayList<FormHolder>();
			Parser p = parse();
			CssSelectorNodeFilter filter = new CssSelectorNodeFilter("form");
			try {
				NodeList nl = p.extractAllNodesThatMatch(filter);
				for (int i = 0; i < nl.size(); i++) {
					Node n = nl.elementAt(i);
					if (n instanceof FormTag) {
						FormHolder fh = buildFormHolder((FormTag) n);
						if (fh != null)
							forms.add(fh);
					}
				}
			} catch (ParserException e) {
				logger.debug("Error parsing page [Title: " + title + "]", e);
			}
		}
		return forms;
	}

	public Form getFormByName(String name) {
		if (name == null)
			return null;
		List<FormHolder> l = getFormList();
		for (FormHolder fh : l) {
			if (name.equals(fh.name))
				return fh.form;
		}
		return null;
	}

	public Form[] getAllForms() {
		List<FormHolder> l = getFormList();
		Form[] ret = new Form[l.size()];
		for (int i = 0; i < l.size(); i++) {
			ret[i] = l.get(i).form;
		}
		return ret;
	}

	public Form getFormById(String id) {
		if (id == null)
			return null;
		List<FormHolder> l = getFormList();
		for (FormHolder fh : l) {
			if (id.equals(fh.id))
				return fh.form;
		}
		return null;
	}

	public String getMetaByName(String name) {
		if (name == null)
			return null;
		for (MetaHolder mh : metas) {
			if (name.equals(mh.name))
				return mh.content;
		}
		return null;
	}

	public String getMetaByHttpEquiv(String httpEquiv) {
		if (httpEquiv == null)
			return null;
		for (MetaHolder mh : metas) {
			if (httpEquiv.equals(mh.httpEquiv))
				return mh.content;
		}
		return null;
	}

	public String[] extractAllLinks() {
		Refiner r = refine();
		r.extractTags("a").extractAttributesFromTags("href");
		String[] strs = r.getResults();
		List<String> list = new ArrayList<String>();
		for (int i = 0; i < strs.length; i++) {
			try {
				new URL(strs[i]);
				list.add(strs[i]);
			} catch (MalformedURLException e) {
			}
		}
		String[] ret = new String[list.size()];
		list.toArray(ret);
		return ret;
	}

	public Refiner refine() {
		return new Refiner(html);
	}

	public Parser parse() {
		Lexer lexer = new Lexer(new org.htmlparser.lexer.Page(html));
		Parser parser = new Parser(lexer, Parser.DEVNULL);
		return parser;
	}

	private class FormHolder {
		public Form form;
		public String name;
		public String id;
	}

	private class MetaHolder {
		public String content;
		public String name;
		public String httpEquiv;
	}
}
